import pandas

import coin.exchange.base.order_gateway as ogbase
import coin.exchange.base.rest_order_gateway_base as rogbase
import coin.strategy.executor.util.traits as extraits
from coin.strategy.executor.passive.simple_mm_executor import SimpleMMExecutor
from coin.strategy.executor.aggressive.aggressive_executor import AggressiveExecutor
from coin.strategy.executor.util.cancel_executor import CancelExecutor
from coin.strategy.executor.util.config_adjuster import ExecutorConfigAdjuster
from coin.strategy.executor.util.last_fire_timestamp import LastFireTimestamp
from coin.strategy.executor.util.last_order_state import LastOrderState
from coin.proto.coin_executor_pb2 import AggressiveExecutorConfig
from coin.proto.coin_executor_pb2 import PassiveExecutorConfig
from coin.strategy.executor.util.order_adjuster import ProductInfoAdjuster
from coin.exchange.base.order_gateway import OrderSide
from coin.strategy.executor.util.working_order_state import (WorkingOrderStateGenerator)
from coin.exchange.base.order_gateway import BaseOrderGatewayInfoMutator
from coin.exchange.base.kr_rest.currency import NormCurrency
from coin.exchange.util.rate_limit import RateLimiter
from coin.strategy.executor.util.last_fill_timestamp import LastFillTimestamp
from coin.exchange.base.order_gateway_util import (create_order_internal)


class SimOrderGatewayInfo(BaseOrderGatewayInfoMutator):
  BalanceType = float
  PositionType = None

  def __init__(self, currencies, products):
    # do not call super init
    self._currencies = currencies
    self._products = products
    self._balances = {currency.symbol: None for currency in currencies}
    self._positions = {product.symbol: None for product in products}
    self._balances_internal = {currency.symbol: None for currency in currencies}
    self._positions_internal = {product.symbol: None for product in products}

  def update_balance_userinfo(self, data):
    pass

  def update_balance_sub(self, data):
    pass


def generate_sim_order(worder, product):
  order = ogbase.Order(order_id=worder.order_id,
                       product=product,
                       price=worder.price,
                       qty=worder.remain_qty,
                       order_side=OrderSide.BUY if worder.sign > 0 else OrderSide.SELL,
                       accepted=True,
                       cancel_sent=False,
                       order_type=ogbase.OrderType.LIMIT,
                       tag="",
                       internal=create_order_internal(
                           timestamp=worder.accepted_time,
                           exchange_order_id=worder.order_id,
                       ))
  return order


def generate_sim_order_from_fill_response(response, product):
  order = ogbase.Order(order_id=response.order_id,
                       product=product,
                       price=response.order_price,
                       qty=0,
                       order_side=OrderSide.BUY if response.sign > 0 else OrderSide.SELL,
                       accepted=True,
                       cancel_sent=False,
                       order_type=ogbase.OrderType.LIMIT,
                       tag="",
                       internal=create_order_internal(
                           timestamp=response.submit_time,
                           exchange_order_id=response.order_id,
                       ))
  return order


def get_market_type(product):
  if product.product_type == "Spot":
    # CurrencyPair may be traded as margin, but ignore for now.
    return "Spot"
  elif product.product_type == "Futures":
    return "Futures"
  else:
    raise ValueError(product)


class LmSimOrderGateway(ogbase.OrderGatewayBase):
  def __init__(self, logger, simulator, product, pindex, post_only):
    ogbase.OrderGatewayBase.__init__(self)
    self._logger = logger
    self._og_info = SimOrderGatewayInfo([NormCurrency(product._quote.currency)], [product])
    self._simulator = simulator
    self._product = product
    self._pindex = pindex
    self._cancel_sent = dict()
    self._market_type = get_market_type(product)
    self._exchange = product.exchange
    self._state_changed = True
    self.post_only = post_only
    self.pyworders = []
    self._initial_position = None

  def is_ready(self):
    return True

  def set_initial_position(self, product, initial_position):
    if self._initial_position is None:
      self._initial_position = {}
    self._initial_position[product] = initial_position

  def get_initial_position(self, product):
    if self._initial_position is None:
      return 0
    return self._initial_position.get(product, 0)

  def get_position(self, product):
    assert product == self._product
    init_pos = self.get_initial_position(product)

    return self._simulator.GetPosition(self._pindex) + init_pos

  def set_timestamp(self, timestamp):
    self._timestamp = timestamp
    self._state_changed |= self.flush_response()

  def flush_response(self):
    changed = False
    responses = self._simulator.GetResponse(self._pindex)
    if responses is not None:
      for response in responses.responses:
        if response.type == 2:  # filled. hack. sorry.
          filled_order = generate_sim_order_from_fill_response(response, self._product)
          self._publish_fill(response.fill_price, response.fill_qty, filled_order)
      changed = True
    return changed

  @property
  def tsstr(self):
    if self._timestamp is None:
      return ""
    else:
      return pandas.DatetimeIndex([self._timestamp])[0]

  def get_working_order(self):
    self.update_working_order()
    return self.pyworders

  def update_working_order(self):
    if self._state_changed:
      worders = self._simulator.GetRemainingOrders(self._pindex)
      self._state_changed = False
    else:
      return
    if worders is None:
      self.pyworders = []
      return
    pyworders = []
    for worder in worders.orders:
      pyworder = generate_sim_order(worder, self._product)
      if pyworder.order_id in self._cancel_sent:
        pyworder.cancel_sent = True
      pyworders.append(pyworder)
    self.pyworders = pyworders

  def submit_order(self, product, price, qty, order_sign, force_fire):
    assert product == self._product
    # self._logger.info("%20s| %s %s submit %4s %8.2f@%5.4f" % (self.tsstr, self._pindex, product, ("buy" if order_sign > 0 else "sell"), qty, price))
    if force_fire:
      post_only = False
    else:
      post_only = self.post_only
    self._simulator.SendOrder(self._pindex, price, qty, order_sign, post_only=post_only)

  def cancel_order(self, order_id):
    if order_id not in self._cancel_sent:
      # self._logger.info("%20s| %s cancel sent on %s" % (self.tsstr, self._pindex, order_id))
      self._simulator.CancelOrder(self._pindex, order_id)
      self._cancel_sent[order_id] = True

  def cancel_all(self):
    for wo in self.get_working_order():
      if not wo.cancel_sent:
        self._simulator.CancelOrder(self._pindex, wo.order_id)


class LmSimSimpleTraits(extraits.FuturesExecutorTraits):
  BuyOrderSide = [OrderSide.BUY]
  SellOrderSide = [OrderSide.SELL]

  def _submit_order(self, product, price, qty, order_side, force_fire=False):
    self.og.submit_order(product,
                         price,
                         qty,
                         1 if order_side in self.BuyOrderSide else -1,
                         force_fire=force_fire)

  def _cancel_orders(self, order_id_list):
    for order_id in order_id_list:
      self.og.cancel_order(order_id)


class LmSimAggExecutor(AggressiveExecutor):
  OGType = LmSimOrderGateway
  Traits = LmSimSimpleTraits

  def __init__(self, product, executor_config, order_gateway, logger, latency_recorder=None):
    # do not call super init
    assert isinstance(order_gateway, self.OGType)
    assert type(executor_config) == AggressiveExecutorConfig, type(executor_config)
    self._config2 = ExecutorConfigAdjuster(executor_config)
    self._logger = logger
    self._latency_recorder = latency_recorder
    self._product = product
    self._product_adjuster = ProductInfoAdjuster(self._product)
    self._og = order_gateway
    self._og.set_latency_recorder(self._latency_recorder)
    self._traits = self.Traits(self._config2, order_gateway, self._product)

    self._logger.info('product: %s', self._product)
    self._logger.info("executor config:\n%s", self._config2)

    self._position = None
    self._close_only = False
    self._wo = WorkingOrderStateGenerator(product=self._product,
                                          og=self._og,
                                          sell_order_side=self._traits.SellOrderSide,
                                          buy_order_side=self._traits.BuyOrderSide)
    self._cancel_executor = CancelExecutor(self._wo)
    self._lo = LastOrderState(self._config2.config.order_update_period, 0)
    self._lfiret = LastFireTimestamp(self._config2.config)
    self._reset_action()

  def set_timestamp(self, timestamp):
    self._og.set_timestamp(timestamp)

  def manage_agg_orders_impl(self):
    timestamp = self._og._timestamp

    self._update_state()
    self._reset_action()
    self._manage_risk(timestamp)

    self._cancel_executor.remove_duplicates()
    if self._cancel_executor.has_cancel():
      self._traits._cancel_orders(self._cancel_executor.get_cancel_list())

  def submit_agg_order_impl(self, *, order_side, order_price, order_qty, timestamp=None):
    timestamp = timestamp or self._og._timestamp
    self._update_state()
    self._reset_action()

    if order_side == OrderSide.BUY:
      return self._try_aggression_buy(timestamp=timestamp, buy_price=order_price, buy_qty=order_qty)
    elif order_side == OrderSide.SELL:
      return self._try_aggression_sell(timestamp=timestamp,
                                       sell_price=order_price,
                                       sell_qty=order_qty)
    else:
      raise ValueError(order_side)
      return False


class SimRateLimiter(object):
  def __init__(self, rate_cnt, per, base_time=None):
    self.rate_cnt = float(rate_cnt)
    # unit: messages
    self.per = float(per)
    # unit: seconds
    self.allowance = rate_cnt
    # unit: messages
    if base_time is None:
      base_time = time.time()
    self.last_check = base_time * 1e-9  # floating-point, e.g. usec accuracy. Unit: seconds
    self.last_timestamp = base_time * 1e-9

  def set_timestamp(self, timestamp):
    self.last_timestamp = timestamp * 1e-9

  def check(self):
    current = self.last_timestamp
    rate_discount = 1.0
    time_passed = (current - self.last_check)
    if time_passed <= 0:
      return False
    self.last_check = current
    self.allowance += time_passed * (self.rate_cnt / self.per)
    if (self.allowance > self.rate_cnt):
      self.allowance = self.rate_cnt
      # throttle
    if (self.allowance < rate_discount):
      return False
    else:
      self.allowance -= rate_discount
      return True


class LmSimMMExecutor(SimpleMMExecutor):
  OGType = LmSimOrderGateway
  Traits = LmSimSimpleTraits

  def __init__(self, product, executor_config, order_gateway, logger, latency_recorder=None):
    assert isinstance(order_gateway, self.OGType), (order_gateway, self.OGType)
    assert type(executor_config) == PassiveExecutorConfig, type(executor_config)
    self._config2 = ExecutorConfigAdjuster(executor_config)
    self._config2.config.allow_amend = False
    self._logger = logger
    self._product = product
    self._product_adjuster = ProductInfoAdjuster(self._product)
    self._og = order_gateway
    self._traits = self.Traits(self._config2, order_gateway, self._product)

    self._logger.info('product: %s', self._product)
    self._logger.info("executor config:\n%s", self._config2)
    self._latency_recorder = latency_recorder
    self._og.set_latency_recorder(self._latency_recorder)

    self._position_timestamp = None
    self._position = None
    self._close_only = False
    self._wo = WorkingOrderStateGenerator(product=self._product,
                                          og=self._og,
                                          sell_order_side=self._traits.SellOrderSide,
                                          buy_order_side=self._traits.BuyOrderSide)
    self._cancel_executor = CancelExecutor(self._wo)
    self._sell_rate_limiter = None
    self._buy_rate_limiter = None
    if self._config2.config.HasField('fill_limiter_qty'):
      self._sell_fill_limiter_qty = QtyLimiter(self._config2.fill_limiter_qty,
                                               self._config2.fill_limiter_per)
      self._buy_fill_limiter_qty = QtyLimiter(self._config2.fill_limiter_qty,
                                              self._config2.fill_limiter_per)
    else:
      self._sell_fill_limiter_qty = None
      self._buy_fill_limiter_qty = None
    self._lfiret = LastFireTimestamp(self._config2.config)
    self._lfillt = LastFillTimestamp(self._config2.config)
    self._reset_action()

  def set_timestamp(self, timestamp):
    self._og.set_timestamp(timestamp)
    if self._sell_rate_limiter is None:
      self._sell_rate_limiter = SimRateLimiter(self._config2.config.rate_limiter_rate,
                                               self._config2.config.rate_limiter_per,
                                               timestamp)
      self._buy_rate_limiter = SimRateLimiter(self._config2.config.rate_limiter_rate,
                                              self._config2.config.rate_limiter_per,
                                              timestamp)
    else:
      self._sell_rate_limiter.set_timestamp(timestamp)
      self._buy_rate_limiter.set_timestamp(timestamp)

    self.timestamp = timestamp

  def force_fire_sell_order(self, sell_price, sell_qty):
    self._traits._submit_order(self._product, sell_price, sell_qty, OrderSide.SELL, force_fire=True)

  def force_fire_buy_order(self, buy_price, buy_qty):
    self._traits._submit_order(self._product, buy_price, buy_qty, OrderSide.BUY, force_fire=True)

  def send_cancels(self):
    if self._cancel_executor.has_cancel():
      self._traits._cancel_orders(self._cancel_executor.get_cancel_list())
    self._reset_action()

  def manage2_mm_orders_impl(self):
    if not self._og.is_ready():
      self._logger.info('OG is not ready, probably due to trade client restart!')
      return

    self._update_state(self.timestamp)
    self._reset_action()
    self._manage_risk(self.timestamp)
    self._cancel_executor.remove_duplicates()
    self.send_cancels()

  def _update_state(self, timestamp):
    prev_position = self._position
    position = self._traits._get_position(self._product)
    pos_change = (type(prev_position) != type(position) or prev_position != position)
    if pos_change:
      # TODO(jshin): not yet possible to differentiate buy/sell fill. set both.
      self._lfillt.set_last_sell_fill(timestamp)
      self._lfillt.set_last_buy_fill(timestamp)
    if pos_change and self._sell_fill_limiter_qty and prev_position is not None:
      delta_position = position - prev_position
      self._sell_fill_limiter_qty.check(timestamp, -delta_position)
      self._buy_fill_limiter_qty.check(timestamp, delta_position)

    self._position_timestamp = timestamp
    self._position = position
    self._wo._update_order_state()

  def submit_mm_orders_impl(self,
                            *,
                            sell_price,
                            buy_price,
                            sell_qty,
                            buy_qty,
                            sell_price_pull,
                            buy_price_pull,
                            sell_price_push,
                            buy_price_push,
                            timestamp,
                            cancel_all_existing_sell=False,
                            cancel_all_existing_buy=False):
    if not self._og.is_ready():
      self._logger.info('OG is not ready, probably due to trade client restart!')
      return

    sell_amend_order = None
    buy_amend_order = None

    self._update_state(timestamp)
    if self._config2.config.allow_amend:
      buy_amend_order = self._cancel_executor.extract_buy()
      sell_amend_order = self._cancel_executor.extract_sell()
    self._reset_action()

    sell_price, buy_price, sell_qty, buy_qty = self._adjust_request(sell_price, buy_price, sell_qty,
                                                                    buy_qty)
    '''
      ret1 = self._submit_sell_order(
        timestamp=timestamp,
        sell_price=sell_price,
        sell_qty=sell_qty,
        sell_price_push=sell_price_push,
        sell_price_pull=sell_price_pull)
      ret2 = self._submit_buy_order(
        timestamp=timestamp,
        buy_price=buy_price,
        buy_qty=buy_qty,
        buy_price_push=buy_price_push,
        buy_price_pull=buy_price_pull)
        '''
    ret1 = self._submit_sell_order(timestamp=timestamp, sell_price=sell_price, sell_qty=sell_qty)
    ret2 = self._submit_buy_order(timestamp=timestamp, buy_price=buy_price, buy_qty=buy_qty)

    # print(ret1, ret2)

    if self._fire_sell or self._fire_buy:
      if self._lfiret.num_fires % 2 == 0:
        self._try_fire_sell_order(sell_price, sell_qty, sell_amend_order)
        self._try_fire_buy_order(buy_price, buy_qty, buy_amend_order)
      else:
        self._try_fire_buy_order(buy_price, buy_qty, buy_amend_order)
        self._try_fire_sell_order(sell_price, sell_qty, sell_amend_order)

    if sell_price_pull is not None or sell_price_push is not None:
      # TODO: this assert may fail due to price adjust.
      # assert sell_price > sell_price_pull, (sell_price, sell_price_pull)
      self._cancel_executor.cancel_sell_by_price(sell_price_pull, sell_price_push)
    if buy_price_pull is not None or buy_price_push is not None:
      # TODO: this assert may fail due to price adjust.
      # assert buy_price < buy_price_pull, (buy_price, buy_price_pull)
      self._cancel_executor.cancel_buy_by_price(buy_price_pull, buy_price_push)

    if cancel_all_existing_sell:
      self._cancel_executor.cancel_sell_working_orders()
    if cancel_all_existing_buy:
      self._cancel_executor.cancel_buy_working_orders()

    self._cancel_executor.remove_duplicates()
    if self._cancel_executor.has_cancel():
      self._traits._cancel_orders(self._cancel_executor.get_cancel_list())

    ret = False

    if self._fire_sell:
      self._lfiret.set_last_sell_fire(timestamp)
      ret = True

    if self._fire_buy:
      self._lfiret.set_last_buy_fire(timestamp)
      ret = True

    self._traits._check_state_impl()

    return ret

  def submit_mm_orders(self,
                       *,
                       sell_price,
                       buy_price,
                       sell_qty=None,
                       buy_qty=None,
                       sell_price_pull=None,
                       buy_price_pull=None,
                       sell_price_push=None,
                       buy_price_push=None,
                       timestamp=None,
                       cancel_all_existing_sell=False,
                       cancel_all_existing_buy=False):
    timestamp = timestamp or self._og._timestamp
    # It merely checks the obvious validity of the input. Do not
    # adjust inputs in the interface level.

    if sell_price is not None:
      assert sell_price > 0
    if buy_price is not None:
      assert buy_price > 0
    if sell_price is not None and buy_price is not None:
      assert sell_price > buy_price, (sell_price, buy_price)

    if sell_qty is not None:
      assert sell_qty > 0
    if buy_qty is not None:
      assert buy_qty > 0
    # if sell_qty is not None and buy_qty is not None:
    # allow up to 3x
    # assert abs(sell_qty - buy_qty) / (sell_qty + buy_qty) < 0.5

    if sell_price is not None and sell_price_pull is not None:
      assert sell_price > sell_price_pull, (sell_price, sell_price_pull)
    if buy_price is not None and buy_price_pull is not None:
      assert buy_price < buy_price_pull, (buy_price, buy_price_pull)
    if sell_price is not None and sell_price_push is not None:
      assert sell_price < sell_price_push, (sell_price, sell_price_push)
    if buy_price is not None and buy_price_push is not None:
      assert buy_price > buy_price_push, (buy_price, buy_price_push)

    ret = self.submit_mm_orders_impl(sell_price=sell_price,
                                     buy_price=buy_price,
                                     sell_qty=sell_qty,
                                     buy_qty=buy_qty,
                                     sell_price_pull=sell_price_pull,
                                     buy_price_pull=buy_price_pull,
                                     sell_price_push=sell_price_push,
                                     buy_price_push=buy_price_push,
                                     timestamp=timestamp,
                                     cancel_all_existing_sell=cancel_all_existing_sell,
                                     cancel_all_existing_buy=cancel_all_existing_buy)
    return ret

  def manage_mm_orders(self,
                       sell_price,
                       buy_price,
                       *,
                       sell_qty=None,
                       buy_qty=None,
                       sell_price_pull=None,
                       buy_price_pull=None,
                       sell_price_push=None,
                       buy_price_push=None,
                       cancel_all_existing_sell=False,
                       cancel_all_existing_buy=False):
    self.submit_mm_orders(sell_price=sell_price,
                          buy_price=buy_price,
                          sell_qty=sell_qty,
                          buy_qty=buy_qty,
                          sell_price_pull=sell_price_pull,
                          buy_price_pull=buy_price_pull,
                          sell_price_push=sell_price_push,
                          buy_price_push=buy_price_push,
                          cancel_all_existing_sell=cancel_all_existing_sell,
                          cancel_all_existing_buy=cancel_all_existing_buy)
    self.manage2_mm_orders_impl()

  def _manage_risk(self, timestamp):
    max_sell_qty, max_buy_qty = self._adjust_qty(
        self.config2.lot_size * self.config.max_working_cnt,
        self.config2.lot_size * self.config.max_working_cnt)
    self._cancel_executor.cancel_sell_with_risk(self._position, self.config2.min_pos, max_sell_qty)
    self._cancel_executor.cancel_buy_with_risk(self._position, self.config2.max_pos, max_buy_qty)
    self._cancel_executor.cancel_old_orders(timestamp - self.config.max_posting_period)

  def _adjust_qty(self, sell_qty, buy_qty):
    # adjust default size
    if sell_qty is not None:
      sell_qty = sell_qty or self.config2.lot_size
      assert sell_qty > 0
    if buy_qty is not None:
      buy_qty = buy_qty or self.config2.lot_size
      assert buy_qty > 0

    # adjust log2_buy_sell_ratio
    if self.config.log2_buy_sell_ratio > 0:
      # keep buy, reduce sell.
      factor = 2**abs(self.config.log2_buy_sell_ratio)
      if sell_qty is not None:
        sell_qty /= factor
    elif self.config.log2_buy_sell_ratio < 0:
      # keep sell, reduce buy.
      factor = 2**abs(self.config.log2_buy_sell_ratio)
      if buy_qty is not None:
        buy_qty /= factor
    else:
      # no modification
      pass

    # adjust close_only
    if self.config2.close_only:
      pos = self._position - self.config2.reserve
      if pos > 0:
        sell_qty = min(sell_qty, abs(pos))
        buy_qty = 0
      elif pos < 0:
        buy_qty = min(buy_qty, abs(pos))
        sell_qty = 0
      else:
        sell_qty = 0
        buy_qty = 0

    # adjust min_pos, max_pos
    if self._position is not None:
      if sell_qty is not None:
        sell_qty = min(sell_qty, max(0, (self._position - self.config2.min_pos)))
      if buy_qty is not None:
        buy_qty = min(buy_qty, max(0, (self.config2.max_pos - self._position)))

    return sell_qty, buy_qty
